/*
This file is part of lAPIn.

lAPIn is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

lAPIn is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with lAPIn.  If not, see <http://www.gnu.org/licenses/>.
*/
package org.opensource.nabaztag;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import org.opensource.nabaztag.util.URLUtil;
import org.opensource.nabaztag.util.XMLUtil;

/**
 * Main class allowing to interact with a Nabaztag identified by its serial number and token.
 * <br />
 * Based on <a href="http://doc.nabaztag.com/api/home.html">API documentation</a>.
 */
public class Rabbit {

    /**
     * Nabaztag version.
     * <br />
     * Nabaztag is identified as V1 and Nabaztag:tag as V2.
     */
    public enum Version {
        V1, V2, UNKNOWN
    }

    /**
     * Identifies LEDs.
     */
    public enum LED {
        BOTTOM(0),
        LEFT(1),
        MIDDLE(2),
        RIGHT(3),
        TOP(4);

        private int code;

        LED(final int code) {
            this.code = code;
        }

        public int getCode() {
            return this.code;
        }
        
    }

    /**
     * Identifies ears.
     */
    public enum EAR {
        LEFT,
        RIGHT;

        /**
         * Ears rotation motion.
         */
        public enum ROTATION {
            CLOCKWISE,
            COUNTER_CLOCKWISE
        }
    }

    private static final String DEFAULT_NABAZTAG_API_URL = "http://api.nabaztag.com/vl/FR/api.jsp";
    private static final String DEFAULT_NABAZTAG_API_STREAM_URL = "http://api.nabaztag.com/vl/FR/api_stream.jsp";

    private URL nabaztagAPI;
    private URL nabaztagStreamAPI;
    private final String sn;
    private final String token;
    private final StringBuilder command = new StringBuilder();

    /**
     * Construts a Rabbit identied by its serial number and associated token.
     * <br />
     * Your rabbit must be configured to allow external events reception.
     *
     * @param sn your rabbit serial number
     * @param token your rabbit security tokenMalformedURLException
     */
    public Rabbit(final String sn, final String token) {
        if (sn == null) {
            throw new IllegalArgumentException("sn is null");
        }
        if (token == null) {
            throw new IllegalArgumentException("token is null");
        }
        this.sn = sn;
        this.token = token;
        try {
            this.nabaztagAPI = new URL(Rabbit.DEFAULT_NABAZTAG_API_URL);
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Cannot resolve <"+Rabbit.DEFAULT_NABAZTAG_API_URL+">", e);
        }
        try {
            this.nabaztagStreamAPI = new URL(Rabbit.DEFAULT_NABAZTAG_API_STREAM_URL);
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException("Cannot resolve <"+Rabbit.DEFAULT_NABAZTAG_API_STREAM_URL+">", e);
        }
    }

    /**
     * Sets the Nabaztag API URL. Will be used in all subsequent calls.
     * 
     * @param url Nabaztag API URL
     */
    public void setNabaztagAPI(final URL url) {
        this.nabaztagAPI = url;
    }

    /**
     * Sets the Nabaztag Stream API URL. Will be used in all subsequent calls.
     *
     * @param url Nabaztag Stream API URL
     */
    public void setNabaztagStreamAPI(final URL url) {
        this.nabaztagStreamAPI = url;
    }

    /**
     * Executes a command to a specific API URL.
     *
     * @param API API used
     * @param command command to execute
     * @throws IOException if a communication exception occurs while sending the command
     */
    protected String executeCommand(final URL API, final String command) throws IOException {
        final String arguments = "sn="+sn+"&token="+token+"&"+command;
        final String result = URLUtil.get(API, arguments);
        return result;
    }

    /**
     * Executes a command using Rabbit.DEFAULT_NABAZTAG_API_URL API.
     *
     * @param command command to execute
     * @throws IOException if a communication exception occurs while sending the command
     */
    private String executeAPICommand(final String command) throws IOException {
        return executeCommand(this.nabaztagAPI, command);
    }

    /**
     * Executes an action.
     *
     * @param code action code to execute
     * @throws IOException if a communication exception occurs while sending the command
     */
    private String executeAction(final int code) throws IOException {
        if (code < 0 || code > 14) {
            throw new IllegalArgumentException("Unknown action <"+code+">; must be in the range 1-14");
        }

        return executeAPICommand("action="+String.valueOf(code));
    }

    /**
     * Executes a command using Rabbit.DEFAULT_NABAZTAG_API_STREAM_URL API.
     *
     * @param command command sent
     * @throws IOException if a communication exception occurs while sending the command
     */
    private String executeAPIStreamCommand(final String command) throws IOException {
        return executeCommand(this.nabaztagStreamAPI, command);
    }

    /**
     * Pronounces a message.
     *
     * @param message message to be pronounced
     * @return this
     */
    public Rabbit say(final String message) {
        say(message, null);
        return this;
    }

    /**
     * Pronounces a message with a specific voice.
     *
     * @param message message to be pronounced
     * @param voice voice used to pronounce
     * @return this
     */
    public Rabbit say(final String message, final String voice) {
        this.command.append("&tts="+message);
        if (voice != null) {
            this.command.append("&voice="+voice);
        }
        return this;
    }

    /**
     * Sends a message identified by its id.
     *
     * @param id id of the message to send
     * @return this
     */
    public Rabbit sendMessage(final int id) {
        this.command.append("&idmessage="+id);
        return this;
    }

    /**
     * Executes provided choregraphy.
     *
     * @param title title of the choregraphy
     * @param choregraphy choregraphy to be executed
     * @return this
     */
    public Rabbit sendChoregraphy(final String title, final Choregraphy choregraphy) {
        this.command.append("&chor="+choregraphy.getCommand()+"&chortitle="+title);
        return this;
    }

    /**
     * Ensures provided parameter is a valid ear position
     *
     * @param position ear position to be checked
     */
    protected void checkPositionValidity(final int position) {
        if (position < 0 || position > 16) {
            throw new IllegalArgumentException("Position must be comprised in range 0-16");
        }
    }

    /**
     * Sets the position of the right ear.
     *
     * Can only be used once.
     *
     * @param position position of the ear, must be between 0 and 16
     * @return this
     */
    public Rabbit withRightEar(final int position) {
        checkPositionValidity(position);
        this.command.append("&posright="+position);
        return this;
    }

    /**
     * Sets the position of the left ear.
     *
     * Can only be used once.
     *
     * @param position position of the ear, must be between 0 and 16
     * @return this
     */
    public Rabbit withLeftEar(final int position) {
        checkPositionValidity(position);
        this.command.append("&posleft="+position);
        return this;
    }

    /**
     * Publish some content as Nabcast.
     *
     * @param id Nabcast id
     * @param title title of this post in the Nabcast
     * @return this
     */
    public Rabbit asNabcast(final int id, final String title) {
        this.command.append("&nabcast="+id+"&nabcasttitle="+title);
        return this;
    }

    /**
     * Sends the full command.
     *
     * @throws RabbitException if one of the commands cannot be executed
     * @throws IOException if a communication exception occurs while sending the command
     */
    public void go() throws RabbitException, IOException {
        final String result = executeAPICommand(this.command.toString());
        final Message responseMessage = XMLUtil.extractResponseMessage(result);
        if (responseMessage == Message.MESSAGENOTSENT || responseMessage == Message.CHORNOTSENT
                || responseMessage == Message.NABCASTNOTSENT || responseMessage == Message.TTSNOTSENT
                || responseMessage == Message.EARPOSITIONNOTSENT || responseMessage == Message.ABUSESENDING
                || responseMessage == Message.NOGOODTOKENORSERIAL || responseMessage == Message.NOTV2RABBIT) {
            throw new RabbitException(responseMessage, XMLUtil.extractResponseComment(result));
        }
        this.command.delete(0, this.command.length());
    }

    /**
     * Streams one music file represented by url.
     *
     * @param url music file location
     * @throws RabbitException if one of urls cannot be streamed
     * @throws IOException if a communication exception occurs while sending the command
     */
    public void stream(final URL url) throws RabbitException, IOException {
        stream(new URL[]{url});
    }

    /**
     * Streams multiples music files represented by url.
     *
     * @param urls music file locations
     * @throws RabbitException if one of urls cannot be streamed
     * @throws IOException if a communication exception occurs while sending the command
     */
    public void stream(final URL ... urls) throws RabbitException, IOException {
        if (urls.length <= 0) {
            throw new IllegalArgumentException("You have to provide at least one URL");
        }

        final StringBuilder streams = new StringBuilder();
        boolean firstElement = true;
        for (final URL url : urls) {
            if (!firstElement) {
                streams.append("|");
            } else {
                firstElement = false;
            }
            streams.append(url.toString());        
        }
        final String result = executeAPIStreamCommand("urlList="+streams.toString());
        final Message responseMessage = XMLUtil.extractResponseMessage(result);
        if (responseMessage == Message.ABUSESENDING || responseMessage == Message.NOGOODTOKENORSERIAL
                || responseMessage == Message.NOTV2RABBIT || responseMessage == Message.WEBRADIONOTSENT
                || responseMessage == Message.NOCORRECTPARAMETERS) {
            throw new RabbitException(responseMessage, XMLUtil.extractResponseComment(result));
        }
    }

    /**
     * Retrieves a List of all friends.
     *
     * @return a list of all friends
     * @throws IOException if a communication exception occurs while sending the command
     */
    public List<String> getFriends() throws IOException {
        return XMLUtil.extractNodeAttributes(XMLUtil.extractResponse(executeAction(2)), "friend");
    }

    /**
     * Retrieves a List of all blacklisted people.
     *
     * @return a list of all blacklisted people
     * @throws IOException if a communication exception occurs while sending the command
     */
    public List<String> getBlacklist() throws IOException {
        return XMLUtil.extractNodeAttributes(XMLUtil.extractResponse(executeAction(6)), "pseudo");
    }

    /**
     * Retrieves a List of all messages in your inbox.
     *
     * @return a list of all messages in your inbox
     * @throws IOException if a communication exception occurs while sending the command
     */
    public List<String> getMessages() throws IOException {
        return XMLUtil.extractNodeAttributes(XMLUtil.extractResponse(executeAction(3)), "msg");
    }

    /**
     * Retrieves the timezone set.
     *
     * @return the timezone set
     * @throws IOException if a communication exception occurs while sending the command
     */
    public String getTimezone() throws IOException {
        return XMLUtil.extractFirstNodeValue(XMLUtil.extractResponse(executeAction(4)));
    }

    /**
     * Retrieves the signature set.
     *
     * @return the signature set
     * @throws IOException if a communication exception occurs while sending the command
     */
    public String getSignature() throws IOException {
        return XMLUtil.toString(XMLUtil.extractResponse(executeAction(5)).cloneNode(true).getChildNodes().item(0).getChildNodes().item(0));
    }


    /**
     * Returns the Nabaztag's version.
     *
     * @return the Nabaztag's version
     * @throws IOException if a communication exception occurs while sending the command
     */
    public Version getVersion() throws IOException {
        final String version = XMLUtil.extractFirstNodeValue(XMLUtil.extractResponse(executeAction(8)));
        try {            
            return Version.valueOf(version);
        } catch (IllegalArgumentException e) {
            return Version.UNKNOWN;
        }
    }

    /**
     * Returns the Nabaztag's name.
     *
     * @return the Nabaztag's version
     * @throws IOException if a communication exception occurs while sending the command
     */
    public String getName() throws IOException {
        return XMLUtil.extractFirstNodeValue(XMLUtil.extractResponse(executeAction(10)));
    }

    /**
     * Returns sleeping status.
     *
     * @return sleeping status
     * @throws IOException if a communication exception occurs while sending the command
     */
    public boolean isSleeping() throws IOException {
        return "YES".equals(XMLUtil.extractFirstNodeValue(XMLUtil.extractResponse(executeAction(7))));
    }

    /**
     * Send to sleep or wake up.
     *
     * @param sleeping send to sleep if true otherwise wake up
     * @throws IOException if a communication exception occurs while sending the command
     */
    public void setSleeping(final boolean sleeping) throws IOException {
        if (sleeping) {
            executeAction(13);
        } else {
            executeAction(14);
        }
    }

    /**
     * Returns all selected languages.
     *
     * @return a List of all selected languages
     * @throws IOException if a communication exception occurs while sending the command
     */
    public List<String> getSelectedLanguages() throws IOException {
        return XMLUtil.extractNodeAttributes(XMLUtil.extractResponse(executeAction(11)), "lang");
    }

    /**
     * Returns all supported languages.
     *
     * @return a List of supported languages
     * @throws IOException if a communication exception occurs while sending the command
     */
    public List<String> getSupportedLanguages() throws IOException {
        return XMLUtil.extractNodeAttributes(XMLUtil.extractResponse(executeAction(9)), "command");
    }

}
